using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace k_mean_clustering
{
    public abstract class Clustering
    {
        protected TimeSpan m_ComputationTime;
        protected int m_NumberGroups;
        protected int[] m_Groups;
        protected Data m_Data;
        protected double[,] m_Container;
        protected double[,] m_Centroids;
        protected int[] m_SizeGroups;
        protected bool[] m_EmptyGroups; // si "true" alors le groupe est vide
        protected int m_NumberNonEmptyGroups;

        // Accessors 

        public int[] Groups
        {
            get
            {
                return m_Groups;
            }
        }

        public TimeSpan ComputationTime
        {
            get
            {
                return m_ComputationTime;
            }
        }

        private int NumberGroups
        {
            get
            {
                return m_NumberGroups;
            }
        }

        private double[,] Centroids
        {
            get
            {
                return m_Centroids;
            }
        }
        public int NumberNonEmptyGroups
        {
            get
            {
                return m_NumberNonEmptyGroups;
            }
        }

        public Clustering(Data data, int nGroups)
        {
            m_NumberGroups = nGroups;
            m_Data = data;
            m_Container = data.container;
            m_SizeGroups = new int[m_NumberGroups];
            m_Centroids = new double[m_NumberGroups, m_Data.dimension];
            m_EmptyGroups = new bool[m_NumberGroups];

            for (int k = 0; k < m_NumberGroups; k++)
            {
                m_EmptyGroups[k] = false;
            }

        }

        public abstract void LaunchAlgo();


        public void InitializeCentroids(int[] centroids)
        {
            // TO DO : Take care of exceptions where centroids bounds dont match with actual sample

            for (int k = 0; k < m_NumberGroups; k++)
            {
                for (int j = 0; j < m_Data.dimension; j++)
                {
                    m_Centroids[k, j] = m_Container[centroids[k], j];
                }

            }

        }

        public void InitializeRandomCentroids()
        {
            //Méthode qui initialise les moyennes en tirant au hasard des individus 

            List<int> listeIndiv = new List<int>();
            for (int i = 0; i < m_Data.size; i++)
            {
                listeIndiv.Add(i);
            }
            Random random = new Random();
            int[] indice_depart = new int[m_NumberGroups];
            for (int k = 0; k < m_NumberGroups; k++)
            {
                int r = random.Next(0, listeIndiv.Count - 1);
                indice_depart[k] = listeIndiv[r];
                listeIndiv.RemoveAt(r);
                // Ici on fait en sorte que les moyennes soient toutes issues d'individus différents 
            }

            for (int k = 0; k < m_NumberGroups; k++)
            {
                for (int j = 0; j < m_Data.dimension; j++)
                {
                    m_Centroids[k, j] = m_Container[indice_depart[k], j];
                }

            }
        }


        public int FindGroup(int GroupMe)
        {
            // cette méthode permet de trouver le groupe qui minimise 
            //la distance entre l'GroupMe "individu" et la moyenne du groupe
            double min_distance = 0;
            double distance = 0;
            int group = 0;
            int compt = 0;
            for (int k = 0; k < m_NumberGroups; k++)
            {
                if (!m_EmptyGroups[k])
                {
                    compt += 1;
                    for (int j = 0; j < m_Data.dimension; j++)
                    {
                        distance += Math.Pow(m_Container[GroupMe, j] - m_Centroids[k, j], 2);
                    }

                    if (compt == 1)
                    {
                        min_distance = distance;
                        group = 0;
                    }
                    else if (min_distance > distance)
                    {
                        min_distance = distance;
                        group = k;
                    }
                    distance = 0;
                }
            }
            return group;
        }


        public void FindAllGroups(int startIndex, int endIndex)
        {
            // Ici nous sommes obligés de faire un lock pour eviter que la modification des groupes 
            //par l'un des threads ne soit pas prise en compte 
            lock (this)
            {
                for (int i = startIndex; i < endIndex; i++)
                {
                    m_Groups[i] = FindGroup(i);
                }
            }
        }


        public void ResetCentroidsSizeGroups()
        {
            //On initialise a zéro les effectifs  de chaque groupe et chaque moyenne
            for (int k = 0; k < m_NumberGroups; k++)
            {
                m_SizeGroups[k] = 0;
                for (int j = 0; j < m_Data.dimension; j++)
                {
                    m_Centroids[k, j] = 0;
                }
            }
        }

        public void ComputeCentroidsSizeGroups(int startIndex, int endIndex)
        {
            // On fait contribuer chaque individu aux effectifs et aux moyennes
            // pour la 1er partie de la base de données

            //On fait les calculs localement puis on modifie les attribus dans un lock
            // de sorte qu'il n'y ait pas de concurence entre les threads. 

            int[] localEffectifs = new int[m_NumberGroups];
            double[,] localCentroids = new double[m_NumberGroups, m_Data.dimension];
            for (int k = 0; k < m_NumberGroups; k++)
            {
                localEffectifs[k] = 0;
                for (int j = 0; j < m_Data.dimension; j++)
                {
                    localCentroids[k, j] = 0;
                }
            }

            for (int i = startIndex; i < endIndex; i++)
            {
                for (int k = 0; k < m_NumberGroups; k++)
                {
                    if (m_Groups[i] == k)
                    {
                        localEffectifs[k] += 1;
                        for (int j = 0; j < m_Data.dimension; j++)
                        {
                            localCentroids[k, j] += m_Container[i, j];
                        }
                    }
                }
            }

            //Zone critique
            lock (this)
            {
                for (int k = 0; k < m_NumberGroups; k++)
                {
                    m_SizeGroups[k] += localEffectifs[k];
                    for (int j = 0; j < m_Data.dimension; j++)
                    {
                        m_Centroids[k, j] += localCentroids[k, j];
                    }
                }
            }
        }
        public void ClusteringSummary()
        {
            Console.WriteLine("Number of clusters : " + m_NumberGroups);
            for (int i = 0; i < m_NumberGroups; i++)
            {
                Console.WriteLine("-------------------------------------------");
                Console.WriteLine("Cluster " + i);
                Console.WriteLine("-------------------------------------------");
                Console.WriteLine("Number of items " + m_SizeGroups[i]);
                Console.WriteLine("Centroid coordinates : ");
                for (int j = 0; j < m_Data.dimension; j++)
                {
                    Console.WriteLine("[" + j + "] " + m_Centroids[i, j]);
                }

            }
            Console.WriteLine("-------------------------------------------");
            Console.WriteLine("Calculation time : " + m_ComputationTime);

        }


    }

    public class ClusteringMonoThreaded : Clustering
    {

        public ClusteringMonoThreaded(Data data, int nGroups) : base(data, nGroups)
        {
            //Initialisation des atributs de la classe

            for (int k = 0; k < m_NumberGroups; k++)
            {
                m_EmptyGroups[k] = false;
            }

            //Tests avant lancement de l'algo

            if (m_Data.size < m_NumberGroups)
            {
                Console.WriteLine("Error: the number of observation in your data base is smaller than the number of groups wanted.");

            }
            else
            {
                if (m_Data.size < 2)
                {
                    Console.WriteLine("Warning: the number of observation in your data base is smaller than two");
                }
                //LAncement de l'algo de clustering
                DateTime start = DateTime.Now;

                // Initialisation des centroides
                InitializeRandomCentroids();

                LaunchAlgo();
                m_ComputationTime = DateTime.Now - start;

            }

        }
        public ClusteringMonoThreaded(Data data, int nGroups, int[] InitialCentroids) : base(data, nGroups)
        {
            //Initialisation des atributs de la classe
            m_NumberGroups = nGroups;
            m_Data = data;
            m_Container = data.container;
            m_SizeGroups = new int[m_NumberGroups];
            m_Centroids = new double[m_NumberGroups, m_Data.dimension];
            m_EmptyGroups = new bool[m_NumberGroups];
            for (int k = 0; k < m_NumberGroups; k++)
            {
                m_EmptyGroups[k] = false;
            }

            //Tests avant lancement de l'algo


            if (m_Data.size < m_NumberGroups)
            {
                Console.WriteLine("Error: the number of observation in your data base is smaller than the number of groups wanted.");

            }
            else
            {
                if (m_Data.size < 2)
                {
                    Console.WriteLine("Warning: the number of observation in your data base is smaller than two");
                }
                //LAncement de l'algo de clustering
                DateTime start = DateTime.Now;

                // Initialisation des centroides
                InitializeCentroids(InitialCentroids);

                LaunchAlgo();
                m_ComputationTime = DateTime.Now - start;

            }


        }

        public override void LaunchAlgo()
        {


            // A chaque individu, on assigne un groupe
            m_Groups = new int[m_Data.size]; // le ieme individu sera dans le groupe groupe[i]. 
            int[] oldGroups = new int[m_Data.size];
            //On initialise les groupes nouveaus et anciens afin qu'ils soit différents au début. 
            m_Groups[0] = 0;
            oldGroups[0] = 1;
            //La boucle s'arrete quand plus aucun individu ne change de groupe ie: groupe =nouveau_groupe
            while (Enumerable.SequenceEqual(m_Groups, oldGroups) == false)
            {
                m_Groups.CopyTo(oldGroups, 0);

                //Pour chaque individu, on cherche le groupe qui minimise la distance:

                FindAllGroups(0, m_Data.size);

                // Remise à zero des moyennes et des effectifs

                ResetCentroidsSizeGroups();

                //Mise a jour des moyennes dans chaque groupe: 

                ComputeCentroidsSizeGroups(0, m_Data.size);

                //reste alors à normaliser les moyennes en prenant soin de ne pas diviser par zéro. 

                for (int k = 0; k < m_NumberGroups; k++)
                {
                    if (m_SizeGroups[k] == 0)
                    {
                        m_EmptyGroups[k] = true;
                    }
                    else
                    {
                        for (int j = 0; j < m_Data.dimension; j++)
                        {
                            m_Centroids[k, j] = m_Centroids[k, j] / m_SizeGroups[k]; // A gérer: si on a un effectif de groupe nul. 
                        }
                    }
                }
            }

            //On comptabilise le nombre de groupes non vides:
            m_NumberNonEmptyGroups = 0;
            for (int k = 0; k < m_NumberGroups; k++)
            {
                if (!m_EmptyGroups[k])
                {
                    m_NumberNonEmptyGroups += 1;
                }
            }
        }

    }

    public class ClusteringMultiThreaded : Clustering
    {
        private int m_Index; //indice qui coupe la base en deux (m_Data.size/2)

        // Constructors

        public ClusteringMultiThreaded(Data data, int nGroups) : base(data, nGroups)
        {
  
            //Tests avant lancement de l'algo
            

            if (m_Data.size < m_NumberGroups)
            {
                Console.WriteLine("Error: the number of observation in your data base is smaller than the number of groups wanted.");

            }
            else
            {
                if (m_Data.size < 2)
                {
                    Console.WriteLine("Warning: the number of observation in your data base is smaller than two");
                }
                //LAncement de l'algo de clustering
                DateTime start = DateTime.Now;

                // Initialisation des centroides
                InitializeRandomCentroids();
                
                LaunchAlgo();
                m_ComputationTime = DateTime.Now - start;

            }
        }

        public ClusteringMultiThreaded(Data data, int nGroups, int[] InitialCentroids) : base(data,nGroups)
        {

            //Tests avant lancement de l'algo

            if (m_Data.size < m_NumberGroups)
            {
                Console.WriteLine("Error: the number of observation in your data base is smaller than the number of groups wanted.");

            }
            else
            {
                if (m_Data.size < 2)
                {
                    Console.WriteLine("Warning: the number of observation in your data base is smaller than two");
                }
                //LAncement de l'algo de clustering
                DateTime start = DateTime.Now;

                // Initialisation des centroides
                InitializeCentroids(InitialCentroids);

                LaunchAlgo();
                m_ComputationTime = DateTime.Now - start;

            }
            

        }


        public override void LaunchAlgo()
        {

            // A chaque individu, on assigne un groupe
            m_Groups = new int[m_Data.size]; // le ieme individu sera dans le groupe groupe[i]. 
            int[] oldGroups = new int[m_Data.size];
            //On initialise les groupes nouveaus et anciens afin qu'ils soit différents au début. 
            m_Groups[0] = 0;
            oldGroups[0] = 1;
            //La boucle s'arrete quand plus aucun individu ne change de groupe ie: groupe =nouveau_groupe
            while (Enumerable.SequenceEqual(m_Groups, oldGroups) == false)
            {
                m_Groups.CopyTo(oldGroups, 0);

                //Pour chaque individu, on cherche le groupe qui minimise la distance:
                //On lance les deux threads qui trouvent chaque groupe pour chaque indiv
                //L'un pour le haut de la base de donnée, l'autre pour le bas (i > m_Index)
                Thread ThreadFindGroup1 = new Thread(() => FindAllGroups(0, m_Index));
                Thread ThreadFindGroup2 = new Thread(() => FindAllGroups(m_Index, m_Data.size));

                ThreadFindGroup1.Start();
                ThreadFindGroup2.Start();
                // On attend que les deux threads soient finis: 
                ThreadFindGroup1.Join();
                ThreadFindGroup2.Join();
                //on peut alors les tuer et passer à la suite de l'algo:
                ThreadFindGroup1.Abort();
                ThreadFindGroup2.Abort();

                //On lance un thread pour mettre les effectifs et les moyennes à zero: 
                Thread ThreadReset = new Thread(() => ResetCentroidsSizeGroups());
                ThreadReset.Start();
                // on attend que celui-ci soit fini et on le tue:
                ThreadReset.Join();
                ThreadReset.Abort();

                //Mise a jour des moyennes dans chaque groupe: 
                // On créer encore deux nouveaux threads permetant de calculer les effectifs des groupes dans 
                //chaque partie de la base ainsi que la somme des contribution sur chaque variables
                Thread ThreadComputeCentroidsSizeGroups1 = new Thread(() => ComputeCentroidsSizeGroups(0, m_Index));
                Thread ThreadComputeCentroidsSizeGroups2 = new Thread(() => ComputeCentroidsSizeGroups(m_Index, m_Data.size));


                ThreadComputeCentroidsSizeGroups1.Start();
                ThreadComputeCentroidsSizeGroups2.Start();

                ThreadComputeCentroidsSizeGroups1.Join();
                ThreadComputeCentroidsSizeGroups2.Join();

                ThreadComputeCentroidsSizeGroups1.Abort();
                ThreadComputeCentroidsSizeGroups2.Abort();

                //reste alors à normaliser les moyennes en prenant soin de ne pas diviser par zéro. 

                for (int k = 0; k < m_NumberGroups; k++)
                {
                    if (m_SizeGroups[k] == 0)
                    {
                        m_EmptyGroups[k] = true;
                    }
                    else
                    {
                        for (int j = 0; j < m_Data.dimension; j++)
                        {
                            m_Centroids[k, j] = m_Centroids[k, j] / m_SizeGroups[k]; // A gérer: si on a un effectif de groupe nul. 
                        }
                    }
                }
            }

            //On comptabilise le nombre de groupes non vides:
            m_NumberNonEmptyGroups = 0;
            for (int k = 0; k < m_NumberGroups; k++)
            {
                if (!m_EmptyGroups[k])
                {
                    m_NumberNonEmptyGroups += 1;
                }
            }
        }
    }
}